'
' *  Authors:  Benton Stark
' * 
' *  Copyright (c) 2007-2012 Starksoft, LLC (http://www.starksoft.com) 
' *
' * Permission is hereby granted, free of charge, to any person obtaining a copy
' * of this software and associated documentation files (the "Software"), to deal
' * in the Software without restriction, including without limitation the rights
' * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
' * copies of the Software, and to permit persons to whom the Software is
' * furnished to do so, subject to the following conditions:
' * 
' * The above copyright notice and this permission notice shall be included in
' * all copies or substantial portions of the Software.
' * 
' * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
' * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
' * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
' * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
' * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
' * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
' * THE SOFTWARE.
' * 
' 


Imports System.Text
Imports System.Net.Sockets
Imports System.Threading
Imports System.Globalization
Imports System.ComponentModel

Namespace Proxy
    ''' <summary>
    ''' HTTP connection proxy class.  This class implements the HTTP standard proxy protocol.
    ''' <para>
    ''' You can use this class to set up a connection to an HTTP proxy server.  Calling the 
    ''' CreateConnection() method initiates the proxy connection and return a standard
    ''' System.Net.Socks.TcpClient object that can be used as normal.  The proxy plumbing
    ''' is all handled for you.
    ''' </para>
    ''' <code>
    ''' 
    ''' </code>
    ''' </summary>
    Public Class HttpProxyClient
        Private _proxyHost As String
        Private _proxyPort As Integer
        Private _proxyUsername As String
        Private _proxyPassword As String
        Private _respCode As HttpResponseCodes
        Private _respText As String
        Private _tcpClient As TcpClient
        Private _tcpClientCached As TcpClient

        Private Const HTTP_PROXY_DEFAULT_PORT As Integer = 8080
        Private Const HTTP_PROXY_CONNECT_CMD As String = "CONNECT {0}:{1} HTTP/1.0" & vbCr & vbLf & "HOST {0}:{1}" & vbCr & vbLf & vbCr & vbLf
        Private Const HTTP_PROXY_AUTHENTICATE_CMD As String = "CONNECT {0}:{1} HTTP/1.0" & vbCr & vbLf & "HOST {0}:{1}" & vbCr & vbLf & "Proxy-Authorization: Basic {2}" & vbCr & vbLf & vbCr & vbLf

        Private Const WAIT_FOR_DATA_INTERVAL As Integer = 50
        ' 50 ms
        Private Const WAIT_FOR_DATA_TIMEOUT As Integer = 15000
        ' 15 seconds
        Private Const PROXY_NAME As String = "HTTP"

        Private Enum HttpResponseCodes
            None = 0
            [Continue] = 100
            SwitchingProtocols = 101
            OK = 200
            Created = 201
            Accepted = 202
            NonAuthoritiveInformation = 203
            NoContent = 204
            ResetContent = 205
            PartialContent = 206
            MultipleChoices = 300
            MovedPermanetly = 301
            Found = 302
            SeeOther = 303
            NotModified = 304
            UserProxy = 305
            TemporaryRedirect = 307
            BadRequest = 400
            Unauthorized = 401
            PaymentRequired = 402
            Forbidden = 403
            NotFound = 404
            MethodNotAllowed = 405
            NotAcceptable = 406
            ProxyAuthenticantionRequired = 407
            RequestTimeout = 408
            Conflict = 409
            Gone = 410
            PreconditionFailed = 411
            RequestEntityTooLarge = 413
            RequestURITooLong = 414
            UnsupportedMediaType = 415
            RequestedRangeNotSatisfied = 416
            ExpectationFailed = 417
            InternalServerError = 500
            NotImplemented = 501
            BadGateway = 502
            ServiceUnavailable = 503
            GatewayTimeout = 504
            HTTPVersionNotSupported = 505
        End Enum

        ''' <summary>
        ''' Constructor.
        ''' </summary>
        Public Sub New()
        End Sub

        ''' <summary>
        ''' Creates a HTTP proxy client object using the supplied TcpClient object connection.
        ''' </summary>
        ''' <param name="tcpClient">A TcpClient connection object.</param>
        Public Sub New(tcpClient As TcpClient)
            If tcpClient Is Nothing Then
                Throw New ArgumentNullException("tcpClient")
            End If

            _tcpClientCached = tcpClient
        End Sub


        ''' <summary>
        ''' Constructor.  The default HTTP proxy port 8080 is used.
        ''' </summary>
        ''' <param name="proxyHost">Host name or IP address of the proxy.</param>
        Public Sub New(proxyHost As String)
            If [String].IsNullOrEmpty(proxyHost) Then
                Throw New ArgumentNullException("proxyHost")
            End If

            _proxyHost = proxyHost
            _proxyPort = HTTP_PROXY_DEFAULT_PORT
        End Sub

        ''' <summary>
        ''' Constructor.  
        ''' </summary>
        ''' <param name="proxyHost">Host name or IP address of the proxy server.</param>
        ''' <param name="proxyPort">Port number to connect to the proxy server.</param>
        ''' <param name="proxyUsername">Username for the proxy server.</param>
        ''' <param name="proxyPassword">Password for the proxy server.</param>
        Public Sub New(proxyHost As String, proxyPort As Integer, proxyUsername As String, proxyPassword As String)
            If [String].IsNullOrEmpty(proxyHost) Then
                Throw New ArgumentNullException("proxyHost")
            End If

            If [String].IsNullOrEmpty(proxyUsername) Then
                Throw New ArgumentNullException("proxyUsername")
            End If

            If proxyPassword Is Nothing Then
                Throw New ArgumentNullException("proxyPassword")
            End If

            If proxyPort <= 0 OrElse proxyPort > 65535 Then
                Throw New ArgumentOutOfRangeException("proxyPort", "port must be greater than zero and less than 65535")
            End If

            _proxyHost = proxyHost
            _proxyPort = proxyPort
            _proxyUsername = proxyUsername
            _proxyPassword = proxyPassword
        End Sub


        ''' <summary>
        ''' Constructor.  
        ''' </summary>
        ''' <param name="proxyHost">Host name or IP address of the proxy server.</param>
        ''' <param name="proxyPort">Port number for the proxy server.</param>
        Public Sub New(proxyHost As String, proxyPort As Integer)
            If [String].IsNullOrEmpty(proxyHost) Then
                Throw New ArgumentNullException("proxyHost")
            End If

            If proxyPort <= 0 OrElse proxyPort > 65535 Then
                Throw New ArgumentOutOfRangeException("proxyPort", "port must be greater than zero and less than 65535")
            End If

            _proxyHost = proxyHost
            _proxyPort = proxyPort
        End Sub

        ''' <summary>
        ''' Gets or sets host name or IP address of the proxy server.
        ''' </summary>
        Public Property ProxyHost() As String
            Get
                Return _proxyHost
            End Get
            Set(value As String)
                _proxyHost = value
            End Set
        End Property

        ''' <summary>
        ''' Gets or sets port number for the proxy server.
        ''' </summary>
        Public Property ProxyPort() As Integer
            Get
                Return _proxyPort
            End Get
            Set(value As Integer)
                _proxyPort = value
            End Set
        End Property

        ''' <summary>
        ''' Gets String representing the name of the proxy. 
        ''' </summary>
        ''' <remarks>This property will always return the value 'HTTP'</remarks>
        Public ReadOnly Property ProxyName() As String
            Get
                Return PROXY_NAME
            End Get
        End Property

        ''' <summary>
        ''' Gets or sets the TcpClient object. 
        ''' This property can be set prior to executing CreateConnection to use an existing TcpClient connection.
        ''' </summary>
        Public Property TcpClient() As TcpClient
            Get
                Return _tcpClientCached
            End Get
            Set(value As TcpClient)
                _tcpClientCached = value
            End Set
        End Property


        ''' <summary>
        ''' Creates a remote TCP connection through a proxy server to the destination host on the destination port.
        ''' </summary>
        ''' <param name="destinationHost">Destination host name or IP address.</param>
        ''' <param name="destinationPort">Port number to connect to on the destination host.</param>
        ''' <return>
        ''' return an open TcpClient object that can be used normally to communicate
        ''' with the destination server
        ''' </return>
        ''' <remarks>
        ''' This method creates a connection to the proxy server and instructs the proxy server
        ''' to make a pass through connection to the specified destination host on the specified
        ''' port.  
        ''' </remarks>
        Public Function CreateConnection(destinationHost As String, destinationPort As Integer) As TcpClient
            Try
                ' if we have no cached tcpip connection then create one
                If _tcpClientCached Is Nothing Then
                    If [String].IsNullOrEmpty(_proxyHost) Then
                        Throw New ProxyException("ProxyHost property must contain a value.")
                    End If

                    If _proxyPort <= 0 OrElse _proxyPort > 65535 Then
                        Throw New ProxyException("ProxyPort value must be greater than zero and less than 65535")
                    End If

                    '  create new tcp client object to the proxy server
                    _tcpClient = New TcpClient()

                    ' attempt to open the connection
                    _tcpClient.Connect(_proxyHost, _proxyPort)
                Else
                    _tcpClient = _tcpClientCached
                End If

                '  send connection command to proxy host for the specified destination host and port
                SendConnectionCommand(destinationHost, destinationPort)

                ' remove the private reference to the tcp client so the proxy object does not keep it
                ' return the open proxied tcp client object to the caller for normal use
                Dim rtn As TcpClient = _tcpClient
                _tcpClient = Nothing
                Return rtn
            Catch ex As SocketException
                Throw New ProxyException([String].Format(CultureInfo.InvariantCulture, "Connection to proxy host {0} on port {1} failed.", Utils.GetHost(_tcpClient), Utils.GetPort(_tcpClient)), ex)
            End Try
        End Function


        Private Sub SendConnectionCommand(host As String, port As Integer)
            Dim stream As NetworkStream = _tcpClient.GetStream()

            Dim connectCmd As String = CreateCommandString(host, port)

            Dim request As Byte() = ASCIIEncoding.ASCII.GetBytes(connectCmd)

            ' send the connect request
            stream.Write(request, 0, request.Length)

            ' wait for the proxy server to respond
            WaitForData(stream)

            ' PROXY SERVER RESPONSE
            ' =======================================================================
            'HTTP/1.0 200 Connection Established<CR><LF>
            '[.... other HTTP header lines ending with <CR><LF>..
            'ignore all of them]
            '<CR><LF>    // Last Empty Line

            ' create an byte response array  
            Dim response As Byte() = New Byte(_tcpClient.ReceiveBufferSize - 1) {}
            Dim sbuilder As New StringBuilder()
            Dim bytes As Integer = 0
            Dim total As Long = 0

            Do
                bytes = stream.Read(response, 0, _tcpClient.ReceiveBufferSize)
                total += bytes
                sbuilder.Append(System.Text.ASCIIEncoding.UTF8.GetString(response, 0, bytes))
            Loop While stream.DataAvailable

            ParseResponse(sbuilder.ToString())

            '  evaluate the reply code for an error condition
            If _respCode <> HttpResponseCodes.OK Then
                HandleProxyCommandError(host, port)
            End If
        End Sub

        Private Function CreateCommandString(host As String, port As Integer) As String
            Dim connectCmd As String
            If Not String.IsNullOrEmpty(_proxyUsername) Then
                '  gets the user/pass into base64 encoded string in the form of [username]:[password]
                Dim auth As String = Convert.ToBase64String(Encoding.ASCII.GetBytes(String.Format("{0}:{1}", _proxyUsername, _proxyPassword)))

                ' PROXY SERVER REQUEST
                ' =======================================================================
                'CONNECT starksoft.com:443 HTTP/1.0<CR><LF>
                'HOST starksoft.com:443<CR><LF>
                'Proxy-Authorization: username:password<CR><LF>
                '              NOTE: username:password string will be base64 encoded as one 
                '                        concatenated string
                '[... other HTTP header lines ending with <CR><LF> if required]>
                '<CR><LF>    // Last Empty Line
                connectCmd = [String].Format(CultureInfo.InvariantCulture, HTTP_PROXY_AUTHENTICATE_CMD, host, port.ToString(CultureInfo.InvariantCulture), auth)
            Else
                ' PROXY SERVER REQUEST
                ' =======================================================================
                'CONNECT starksoft.com:443 HTTP/1.0 <CR><LF>
                'HOST starksoft.com:443<CR><LF>
                '[... other HTTP header lines ending with <CR><LF> if required]>
                '<CR><LF>    // Last Empty Line
                connectCmd = [String].Format(CultureInfo.InvariantCulture, HTTP_PROXY_CONNECT_CMD & vbCr & vbLf, host, port.ToString(CultureInfo.InvariantCulture))
            End If
            Return connectCmd
        End Function

        Private Sub HandleProxyCommandError(host As String, port As Integer)
            Dim msg As String

            Select Case _respCode
                Case HttpResponseCodes.None
                    msg = [String].Format(CultureInfo.InvariantCulture, "Proxy destination {0} on port {1} failed to return a recognized HTTP response code.  Server response: {2}", Utils.GetHost(_tcpClient), Utils.GetPort(_tcpClient), _respText)
                    Exit Select

                Case HttpResponseCodes.BadGateway
                    'HTTP/1.1 502 Proxy Error (The specified Secure Sockets Layer (SSL) port is not allowed. ISA Server is not configured to allow SSL requests from this port. Most Web browsers use port 443 for SSL requests.)
                    msg = [String].Format(CultureInfo.InvariantCulture, "Proxy destination {0} on port {1} responded with a 502 code - Bad Gateway.  If you are connecting to a Microsoft ISA destination please refer to knowledge based article Q283284 for more information.  Server response: {2}", Utils.GetHost(_tcpClient), Utils.GetPort(_tcpClient), _respText)
                    Exit Select
                Case Else

                    msg = [String].Format(CultureInfo.InvariantCulture, "Proxy destination {0} on port {1} responded with a {2} code - {3}", Utils.GetHost(_tcpClient), Utils.GetPort(_tcpClient), CInt(_respCode).ToString(CultureInfo.InvariantCulture), _respText)
                    Exit Select
            End Select

            '  throw a new application exception 
            Throw New ProxyException(msg)
        End Sub

        Private Sub WaitForData(stream As NetworkStream)
            Dim sleepTime As Integer = 0
            While Not stream.DataAvailable
                Thread.Sleep(WAIT_FOR_DATA_INTERVAL)
                sleepTime += WAIT_FOR_DATA_INTERVAL
                If sleepTime > WAIT_FOR_DATA_TIMEOUT Then
                    Throw New ProxyException([String].Format("A timeout while waiting for the proxy server at {0} on port {1} to respond.", Utils.GetHost(_tcpClient), Utils.GetPort(_tcpClient)))
                End If
            End While
        End Sub

        Private Sub ParseResponse(response As String)
            Dim data As String() = Nothing

            '  get rid of the LF character if it exists and then split the string on all CR
            data = response.Replace(ControlChars.Lf, " "c).Split(ControlChars.Cr)

            ParseCodeAndText(data(0))
        End Sub

        Private Sub ParseCodeAndText(line As String)
            Dim begin As Integer = 0
            Dim [end] As Integer = 0
            Dim val As String = Nothing

            If line.IndexOf("HTTP") = -1 Then
                Throw New ProxyException([String].Format("No HTTP response received from proxy destination.  Server response: {0}.", line))
            End If

            begin = line.IndexOf(" ") + 1
            [end] = line.IndexOf(" ", begin)

            val = line.Substring(begin, [end] - begin)
            Dim code As Int32 = 0

            If Not Int32.TryParse(val, code) Then
                Throw New ProxyException([String].Format("An invalid response code was received from proxy destination.  Server response: {0}.", line))
            End If

            _respCode = CType(code, HttpResponseCodes)
            _respText = line.Substring([end] + 1).Trim()
        End Sub



#Region "Async Methods"

        Private _asyncWorker As BackgroundWorker
        Private _asyncException As Exception
        Private _asyncCancelled As Boolean

        ''' <summary>
        ''' Gets a value indicating whether an asynchronous operation is running.
        ''' </summary>
        ''' <remarks>return true if an asynchronous operation is running; otherwise, false.
        ''' </remarks>
        Public ReadOnly Property IsBusy() As Boolean
            Get
                Return If(_asyncWorker Is Nothing, False, _asyncWorker.IsBusy)
            End Get
        End Property

        ''' <summary>
        ''' Gets a value indicating whether an asynchronous operation is cancelled.
        ''' </summary>
        ''' <remarks>return true if an asynchronous operation is cancelled; otherwise, false.
        ''' </remarks>
        Public ReadOnly Property IsAsyncCancelled() As Boolean
            Get
                Return _asyncCancelled
            End Get
        End Property

        ''' <summary>
        ''' Cancels any asychronous operation that is currently active.
        ''' </summary>
        Public Sub CancelAsync()
            If _asyncWorker IsNot Nothing AndAlso Not _asyncWorker.CancellationPending AndAlso _asyncWorker.IsBusy Then
                _asyncCancelled = True
                _asyncWorker.CancelAsync()
            End If
        End Sub

        Private Sub CreateAsyncWorker()
            If _asyncWorker IsNot Nothing Then
                _asyncWorker.Dispose()
            End If
            _asyncException = Nothing
            _asyncWorker = Nothing
            _asyncCancelled = False
            _asyncWorker = New BackgroundWorker()
        End Sub

        ''' <summary>
        ''' Event handler for CreateConnectionAsync method completed.
        ''' </summary>
        Public Event CreateConnectionAsyncCompleted As EventHandler(Of CreateConnectionAsyncCompletedEventArgs)

        ''' <summary>
        ''' Asynchronously creates a remote TCP connection through a proxy server to the destination host on the destination port.
        ''' </summary>
        ''' <param name="destinationHost">Destination host name or IP address.</param>
        ''' <param name="destinationPort">Port number to connect to on the destination host.</param>
        ''' return an open TcpClient object that can be used normally to communicate
        ''' with the destination server
        ''' <remarks>
        ''' This method creates a connection to the proxy server and instructs the proxy server
        ''' to make a pass through connection to the specified destination host on the specified
        ''' port.  
        ''' </remarks>
        Public Sub CreateConnectionAsync(destinationHost As String, destinationPort As Integer)
            If _asyncWorker IsNot Nothing AndAlso _asyncWorker.IsBusy Then
                Throw New InvalidOperationException("The HttpProxy object is already busy executing another asynchronous operation.  You can only execute one asychronous method at a time.")
            End If

            CreateAsyncWorker()
            _asyncWorker.WorkerSupportsCancellation = True
            AddHandler _asyncWorker.DoWork, New DoWorkEventHandler(AddressOf CreateConnectionAsync_DoWork)
            AddHandler _asyncWorker.RunWorkerCompleted, New RunWorkerCompletedEventHandler(AddressOf CreateConnectionAsync_RunWorkerCompleted)
            Dim args As [Object]() = New [Object](1) {}
            args(0) = destinationHost
            args(1) = destinationPort
            _asyncWorker.RunWorkerAsync(args)
        End Sub

        Private Sub CreateConnectionAsync_DoWork(sender As Object, e As DoWorkEventArgs)
            Try
                Dim args As [Object]() = DirectCast(e.Argument, [Object]())
                e.Result = CreateConnection(DirectCast(args(0), String), CInt(args(1)))
            Catch ex As Exception
                _asyncException = ex
            End Try
        End Sub

        Private Sub CreateConnectionAsync_RunWorkerCompleted(sender As Object, e As RunWorkerCompletedEventArgs)
            RaiseEvent CreateConnectionAsyncCompleted(Me, New CreateConnectionAsyncCompletedEventArgs(_asyncException, _asyncCancelled, DirectCast(e.Result, TcpClient)))
        End Sub



#End Region

    End Class
End Namespace
